Como realizar testing en Angular utilizando el framework the Jasmine
Se mostraran 3 tipos de tests:
Para ejecutar los test y que se nos muestre la cobertura de código tenemos que ejecutar los tests con el siguiente comando:
ng test --code-coverage
La estructura base de un test es la siguiente:
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { Test1Component } from './test1.component';
describe('Test1Component', () => {
let component: Test1Component;
let fixture: ComponentFixture<Test1Component>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ Test1Component ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(Test1Component);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
Para testear el numero de tags creados podemos hacerlo de la siguiente manera
it('should have a row per member', () => {
component.membersList = membersListMock;
fixture.detectChanges();
const compiled = fixture.debugElement.queryAll(
By.css('app-member-row')
).length;
expect(compiled).toBe(4);
});
Utilizamos fixture.debugElement.queryAll para seleccionar las etiquetas que queremos contar
y dentro utilizamos el objeto By.css para indicar el selector css de la etiqeta.
El fixture.debugElement.queryAll devuelve un array con todas las etiquetas coincidentes con el selector, de manera que podemos contarlas con .length
Para testear el contenido de una etiqueta lo hacemos de la siguiente manera:
it('should have a "Members List" header', () => {
const compiled: HTMLElement = fixture.nativeElement.querySelector('h2');
expect(compiled.textContent).toContain('Members List');
});
En este caso utilizamos fixture.nativeElement.querySelector para seleccionar una etiqueta en concreto, y utilizamos la propiedad textContent para leer el contenido de dicha etiqueta
Si queremos testear el contenido de un atributo de una etiqueta lo podemos hacer de la siguiente manera:
Si tenemos una etiqueta como la siguiente:
<h2 ejemplo="valor">Ejemplo de cabecera</h2>
Podemos testear el contenido del atributo ejemplo de la siguiente manera:
it('should have atribute ejemplo with value valor', () => {
const compiled: HTMLElement = fixture.nativeElement.querySelector('h2');
expect(compiled.getAttribute("ejemplo")).toEqual('valor');
});
Si queremos testear si un atributo existe en una etiqueta o no, podemos testearlo contra null tal que así:
it('should have atribute ejemplo with value valor', () => {
const compiled: HTMLElement = fixture.nativeElement.querySelector('h2');
expect(compiled.getAttribute("ejemplo2")).toEqual(null);
});
Para testear el HttpClient primero tenemos que realizar el siguiente import en el test:
imports: [HttpClientTestingModule]
Declaramos un objeto httpTestingController:
httpTestingController = TestBed.inject(HttpTestingController);
Ahora declaramos el test:
it('check HttpClient', () => {
const finalData = [{ userId: 1, id: 1, name: 'nombre', completed: true }];
const req = httpTestingController.expectOne("http://127.0.0.1:8080/api/members");
expect(req.request.method).toEqual('GET');
req.flush(finalData);
expect(component.messageObject).toEqual(finalData);
});
Usamos el httpTestingController.expectOne para indicar que conexion queremos testear, en este caso en el servicio tenemos esto:
Por lo tanto en el expectOne tenemos que indicar la misma dirección
Despues usamos el flush para indicar los datos que recibira la "Solicictud mockeada", en este caso enviamos el objeto finalData y a continuación comprobamos que finalData sea igual a component.messageObject que es la variable del componente donde se almacena la solicitud que recibe.
Si estamos testeando el servicio directamente, tendremos que llamar a la función del servicio en el test, el test quedaría tal que así:
it('check HttpClient', () => {
const finalData = [{ userId: 1, id: 1, name: 'nombre', completed: true }];
service
.getInfo()
.subscribe((data) => expect(data).toEqual(finalData));
const req = httpTestingController.expectOne('http://127.0.0.1:8080/api/members');
expect(req.request.method).toEqual('GET');
req.flush(finalData);
});
Para testear cuando se produce un error con el HttpClient lo hacemos de la siguiente manera:
it('Check ERROR from HTTP server', async () => {
let http = TestBed.inject(HttpTestingController);
let errResponse: any;
const mockErrorResponse = { status: 500, statusText: 'Server not available' };
const data = 'An error occur on the server side';
service.getInfo().subscribe({
error: (e) => (errResponse = e)
});
http.expectOne("http://127.0.0.1:8080/api/members").flush(data, mockErrorResponse);
expect(errResponse.message).toEqual("Error on request");
});
Usamos mockErrorResponse para indicar el mensaje de error y el codigo de respuesta
const mockErrorResponse = { status: 500, statusText: 'Server not available' };
En realidad lo único importante es el codigo de error ya que el mensaje (En este caso concreto) no se utilizará para nada, ya que el error que se lanza es uno concreto puesto en el servicio
De igual manera data tampoco tiene ningun uso, ya que será el cuerpo de la respuesta, pero al ser un error, no lo usaremos para nada
Cuando llamamos al servicio y le asignamos un observable lo tenemos que hacer con el campo de error tal que asi:
service.getInfo().subscribe({
error: (e) => (errResponse = e)
});
El mensaje de error viene del servicio donde se define como se lanza el error:
Cuando se lanza el expect dentro indicamos errResponse.message ya que message es una de las propiedades del objeto Error que está lanzando el servicio, otra manera de compararlo seria la siguiente:
expect(errResponse).toEqual(new Error("Error on request"));
En el caso de que tengamos un servicio que lo tenemos declarado como privado podemos acceder a el con la siguiente sintaxis:
component['variablePrivada']
En el ejemplo anterior estamos almacenando el servicio, que es privado dentro componente, en una variable, esto nos permite utilizar un spyOn sobre el y así saber cuando se llama a la funcion showError del servicio.
Para hacer tests relaccionados con rutas lo hacemos de la siguiente manera:
Primero tenemos que importar el RouterTestingModule:
imports: [RouterTestingModule.withRoutes(routes)]
el parametro routes hace referencia a la tabla de rutas que creamos mas adelante para el test
Creamos un objeto Router:
router = TestBed.inject(Router);
Creamos un objeto Location:
location = TestBed.inject(Location);
Despues tenemos que crear una tabla de rutas para el test:
const routes: Routes = [
{path: 'home', component: MemberProfilePageComponent},
{path: 'main', component: MemberProfilePageComponent},
];
el archivo del test quedaría tal que asi:
Y declaramos el test en si:
it('Test navigation going back when calling redirectToMainPage', fakeAsync (() => {
component.isButtonEnabled = true;
router.navigate(['/home']);
tick();
router.navigate(['/main']);
tick();
component.redirectToMainPage();
tick();
expect(location.path()).toBe('/home');
}));
En este caso estamos testeando la funcionalidad de redirectToMainPage que vuelve a la ruta anterior.
Para simular la navegacion en los tests tenemos que usar fakeAsync y la funcion tick() despues de cada accion de navegación
En este caso estamos navegando a home, despues a main y despues llamamos a redirectToMainPage y al hacer la compobación con location.path() la direccion tendría que ser home
NOTA: El import de Location tiene que ser exactamente el siguiente, sino no funciona (por lo visto hay varios tipos de Location):
import { Location } from '@angular/common';
Para testear una funcion setTimeout tenemos que utilizar fakeAsync y tick para simular el paso del tiempo y que la funcion se dispare
import { fakeAsync, tick} from '@angular/core/testing';
En el test tenemos que pasarle un numero entero a tick para simular el tiempo que queremos que pase
Ejemplo:
Funcion:
Test:
si hay una funcion setTimeout que se ha llamado, tendremos que ejecutar tick con el tiempo necesario para que esa funcion timeout concluya su temporizador y se dispare.
Para testear con el spyOn tenemos que indicar cual es el objeto y su metodo sobre el que poner el spy, una vez tenemos configurado el spy, solo tenemoq que llamar a expect para comprobar si se ha llamado a esa funcion o si se ha llamado con un parametro concreto.
Ejemplo de spyOn de un servicio:
Componente:
Test:
Ejemplo de spyOn de un EventEmitter:
Componente:
Test:
Tenemos que tener en cuenta de llamar a la funcion (en este caso ngOnInit) despues de hacer el spyOn, ya que el componente se crea antes de ejecutar el test y por lo tanto ngOnInit por defecto se llama antes del test (osea antes de aplicar el spyOn), asi que para que se vuelva a ejecutar la funcion (mostrarTexto o el emitter en este caso) sobre la que llamamos al spyOn tenemos que volver a ejecutar el ngOnInit.
Para poder testear una aplicación responsive tenemos que instalar el paquete karma viewport
Para testear si una funciona lanza una excepción lo hacemos de la siguiente manera:
it('check checkValidForm to throw an exception', () => {
expect( function(){ component.checkValidForm(); } ).toThrow(new Error("TEMPLATES.INVALID"));
});
Con ese código testearíamos cuando se lanza una excepción como la siguiente:
Si queremos mockear una funcion lo hacemos de la siguiente manera.
Codigo a testear:
Test:
it('check myMessage is set', () => {
spyOn(component, "setHello").and.returnValue("Hello World!")
component.ngOnInit();
expect(component.myMessage).toEqual('Hello World!');
});
Para mockear una funcion simplemente usamos un spyOn e indicamos el valor a devolver con:
and.returnValue
Si queremos mockear una función que no devuelve nada (void) simplemente hacemos el spyOn con un returnValue vacio:
.and.returnValue()
Si queremos modificar la fecha para algun test que dependa de la hora actual podemos hacerlo asi:
let today = moment("2023-04-02").toDate();
jasmine.clock().mockDate(today);
Test | Jasmine | Angular